﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MoneyWentWhere.DataModel.Binding;
using System.ComponentModel;
using System.Collections;
using System.Runtime.Serialization;

namespace MoneyWentWhere.DataModel
{
    /* Consider:
     * - Add tag count
     */

    // Immutable. Preserves case, but is case-insensitive in comparisons.
    public class Tag : IComparable
    {
        public const char CategoryDelimiter = '/';

        public readonly String Name;

        public Tag(String name)
        {
            String validName = GetValidTagName(name);
            if (String.IsNullOrEmpty(name)) { throw new ArgumentException(String.Format("Not a valid name: '{0}'", name)); }

            Name = validName;
        }

        /// <summary>
        /// Get valid tag name, null if invalid.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static String GetValidTagName(String name)
        {
            if (name == null) { return null; }

            name = name.Trim();
            name = name.Trim(CategoryDelimiter); // remove leading and trailing slashes

            if (name == String.Empty) { return null; } // empty name is invalid

            foreach (char delim in TagSet.DelimiterChars)
            {
                name = name.Replace(delim, '-');
            }

            return name;
        }

        /// <summary>
        /// True if tag matches the filter. 
        /// </summary>
        /// <param name="filter"></param>
        public bool Matches(String filter)
        {
            if (String.IsNullOrEmpty(filter)) { return true; }

            int pos = Name.IndexOf(filter, StringComparison.InvariantCultureIgnoreCase);
            if (pos == 0) { return true; } // single keyword
            if (pos > 0 && Name[pos - 1] == Tag.CategoryDelimiter) { return true; } // category part
            return false;
        }

        public override string ToString() { return Name; }

        public override bool Equals(object obj) 
        {
            Tag that = obj as Tag;
            if (that == null) { return false; }
            return Name.Equals(that.Name, StringComparison.InvariantCultureIgnoreCase); 
        }

        public bool IsAncestorOf(Tag child)
        {
            if (child == null) { return false; }
            if (child.Equals(this)) { return false; }

            return child.Name.StartsWith(this.Name + CategoryDelimiter, 
                StringComparison.InvariantCultureIgnoreCase);
        }

        /// <summary>
        /// Split a tag like a/b/c/d into {a, a/b, a/b/c}
        /// </summary>
        /// <returns></returns>
        public Tag[] GetAncestors()
        {
            String[] names = Name.Split(CategoryDelimiter);
            if (names.Length == 0) { return null; }

            Tag[] tags = new Tag[names.Length - 1];

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < tags.Length; i++)
            {
                if (i > 0) { sb.Append(CategoryDelimiter); }
                sb.Append(names[i]);
                tags[i] = new Tag(sb.ToString());
            }

            return tags;
        }

        public override int GetHashCode() 
        { 
            return Name.ToLowerInvariant().GetHashCode(); 
        }

        public int CompareTo(object obj)
        {
            Tag that = obj as Tag;
            if (that == null) { return -1; }
            return this.Name.CompareTo(that.Name);
        }

    }

    // Immutable
    [TypeConverter(typeof(TagSetTypeConverter))]
    [DataContract]
    public class TagSet : IEnumerable<Tag>
    {
        public static readonly char[] DelimiterChars = new char[] { ' ', ',' };
        public const String DelimiterDefault = " ";

        public static TagSet Empty = new TagSet("");

        // Set containing all tags, including any ancestors.
        HashSet<Tag> _allTags = new HashSet<Tag>();

        // Normalized string representation, no ancestors
        [DataMember(Name = "r")]
        String _stringRep = String.Empty;

        public TagSet(params String[] strings)
        {
            if (strings == null) { return; }

            var tags = strings.Where( x => Tag.GetValidTagName(x) != null ).Select( x=> new Tag(x));
            Add(tags);
        }

        public TagSet(IEnumerable<Tag> tags)
        {
            if (tags == null) { return; }
            Add(tags);
        }

        // Private since class is immutable
        void Add(IEnumerable<Tag> tags)
        {
            // Normalize
            // NOTE: Inefficient O(n^2), refactor if needed
            var normalizedTags = tags.Distinct().Where(x => ContainsNoDescenantsOf(tags,x)).OrderBy(x => x.Name);

            // Add tags and ancestors to hashset
            foreach (Tag tag in normalizedTags )
            {
                _allTags.Add(tag);

                Tag[] ancestors = tag.GetAncestors();
                if (ancestors != null) 
                { 
                    foreach(Tag a in ancestors) { _allTags.Add(a); }
                }
            }

            _stringRep = normalizedTags.ToJoinedString(DelimiterDefault);
        }

        bool ContainsNoDescenantsOf(IEnumerable<Tag> tags, Tag ancestor)
        {
            return tags.FirstOrDefault(x => ancestor.IsAncestorOf(x)) == null;
        }

        public bool Contains(Tag t)
        {
            return _allTags.Contains(t);
        }

        public static TagSet Parse(String rep)
        {
            if (rep == null) { return TagSet.Empty; }
            rep = rep.ToString();
            if (rep == "") { return TagSet.Empty; }
            
            var tags = rep.Split(DelimiterChars, StringSplitOptions.RemoveEmptyEntries).Select( x => new Tag(x));
            return new TagSet(tags);
        }

        public override string ToString() { return _stringRep; }

        public override bool Equals(object obj)
        {
            TagSet that = obj as TagSet;
            if (that == null) { return false; }
            return _stringRep.Equals(that._stringRep, StringComparison.InvariantCultureIgnoreCase);
        }

        public override int GetHashCode()
        {
            return _stringRep.ToLowerInvariant().GetHashCode();
        }

        public IEnumerator<Tag> GetEnumerator()
        {
            return _allTags.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _allTags.GetEnumerator();
        }
    }

    // Global tag registry
    public class TagRegistry : IEnumerable<Tag>
    {
        public Dictionary<Tag, int> _tags = new Dictionary<Tag, int>();

        public void Add(TagSet tagSet)
        {
            foreach (Tag t in tagSet) { Add(t); }
        }

        public void Add(Tag tag)
        {
            if (!_tags.ContainsKey(tag)) { _tags.Add(tag, 0); }
            _tags[tag]++;
        }

        public void Remove(TagSet tagSet)
        {
            foreach (Tag t in tagSet) { Remove(t); }
        }

        public void Remove(Tag tag)
        {
            if (!_tags.ContainsKey(tag)) { return; }
            _tags[tag]--;
        }

        public int Count(Tag tag)
        {
            if (!_tags.ContainsKey(tag)) { return 0; }
            return _tags[tag];
        }

        public IEnumerator<Tag> GetEnumerator()
        {
            return _tags.Keys.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _tags.Keys.GetEnumerator();
        }
    }
}
